﻿using System;
using System.Collections.Generic;
using System.Linq;
using OpenCvSharp;

namespace TestPaper
{
    public class TestPaperLevel
    {
        /// <summary>
        /// 识别试纸等级
        /// </summary>
        /// <param name="fileName">本地文件名</param>
        /// <param name="contrast">左边色条与右边色条的对比度</param>
        /// <param name="level">对比度等级</param>
        /// <param name="saveFileName">这个参数用于批量保存中间图片的路径。null表示不保存中间图片</param>
        /// <returns>返回0则成功，反之异常</returns>
        public static int TestPaper(string fileName, out double contrast, out int level, string saveFileName = null)
        {
            contrast = -1;
            level = -1;
            Mat image = Cv2.ImRead(fileName, ImreadModes.Color);
            Mat image_roi = image[new Rect(image.Cols / 3, 0, image.Cols / 3, image.Rows)].Clone();
            Mat element = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3));
            Cv2.MorphologyEx(image_roi, image_roi, MorphTypes.Erode, element);

            WhiteBalance(ref image_roi);
            Mat hsv = image_roi.CvtColor(ColorConversionCodes.BGR2HSV_FULL);
#if false
            Cv2.ImWrite("v.bmp", image_roi);
#endif
            Point center = LocationCenter(hsv);

            if ((center.X < image_roi.Cols / 4) || (center.X > image_roi.Cols * 3 / 4))
            {
                image_roi = image[new Rect(center.X + image.Cols / 6, 0, image.Cols / 3, image.Rows)].Clone();
                center.X = image_roi.Cols / 2;
                Cv2.MorphologyEx(image_roi, image_roi, MorphTypes.Erode, element);
                WhiteBalance(ref image_roi);
#if false
                Cv2.ImWrite("v1.bmp", image_roi);
#endif
                hsv = image_roi.CvtColor(ColorConversionCodes.BGR2HSV_FULL);
            }

            Rect center_rect = CenterRegion(hsv, center);
            if (center_rect.Bottom > image.Rows - 2 || center_rect.Top < 2)
            {
#if true
                if (saveFileName != null)
                {
                    Cv2.Circle(image_roi, center, 5, Scalar.All(100), 3);
                    Cv2.Rectangle(image_roi, center_rect, Scalar.Green);
                    Cv2.ImWrite(saveFileName, image_roi);
                }
#endif
                return -3;
            }

            if (!FindRightStandard(hsv, center_rect, out int right_x, out byte right_color, out byte background, image_roi))
            {
#if true
                if (saveFileName != null)
                {
                    Cv2.Circle(image_roi, center, 5, Scalar.All(100), 3);
                    Cv2.Rectangle(image_roi, center_rect, Scalar.Green);
                    Cv2.ImWrite(saveFileName, image_roi);
                }
#endif
                return -1;
            }

            if (!FindLeftColor(hsv, center, center_rect, right_x, out byte left_color, out byte backgournd_left, image_roi))
            {
#if true
                if (saveFileName != null)
                {
                    Cv2.Circle(image_roi, center, 5, Scalar.All(100), 3);
                    Cv2.Rectangle(image_roi, center_rect, Scalar.Green);
                    Cv2.ImWrite(saveFileName, image_roi);
                }
#endif
                return -2;
            }

#if true
            if (saveFileName != null)
            {
                Cv2.Circle(image_roi, center, 5, Scalar.All(100), 3);
                Cv2.Rectangle(image_roi, center_rect, Scalar.Green);
                Cv2.ImWrite(saveFileName, image_roi);
            }
#endif
            background = Math.Max(backgournd_left, background);
            contrast = (double)(background - left_color) / (background - right_color);

            if (contrast < m_levelTable[0])
                level = 0;
            else if (contrast >= m_levelTable[0] && contrast < m_levelTable[1])
                level = 1;
            else if (contrast >= m_levelTable[1] && contrast < m_levelTable[2])
                level = 2;
            else if (contrast >= m_levelTable[2] && contrast < m_levelTable[3])
                level = 3;
            else if (contrast >= m_levelTable[3] && contrast < m_levelTable[4])
                level = 4;
            else if (contrast >= m_levelTable[4] && contrast < m_levelTable[5])
                level = 5;
            else if (contrast >= m_levelTable[5] && contrast < m_levelTable[6])
                level = 6;
            else if (contrast >= m_levelTable[6] && contrast < m_levelTable[7])
                level = 7;
            else if (contrast >= m_levelTable[7])
                level = 8;
            return 0;
        }

        private static bool FindLeftColor(Mat hsv, Point center, Rect center_rect, int right_x, out byte color, out byte background, Mat image = null)
        {
            background = 0;
            int stepLength = (int)((right_x - center_rect.Right) * 1.5);
            if (stepLength > center_rect.Left)
                stepLength = center_rect.Left;

            int y = center_rect.Y + center_rect.Height / 2;
            int x_start = center_rect.Left - (right_x - center_rect.Right) / 2;
            Vec3b center_pix = hsv.At<Vec3b>(center.Y, center.X);
            color = 255;
            int x = x_start - 1;
            for (int i = 1; i < stepLength; i++)
            {
                Vec3b pix = hsv.At<Vec3b>(y, x_start - i);
                if (pix[2] > background)
                    background = pix[2];
                //if (pix[0] > 64 && pix[0] < 190 && pix[1] > 20)
                if (Math.Abs(pix[0] - center_pix[0]) < 10 && Math.Abs(pix[1] - center_pix[1]) < 30)
                    continue;
                if (pix[2] < color)
                {
                    color = pix[2];
                    x = x_start - i;
                }
            }
#if true
            if (image != null)
                Cv2.Circle(image, new Point(x, y), 5, Scalar.All(127), 3);
#endif
            return color != 255;
        }

        private static bool FindRightStandard(Mat hsv, Rect center_rect, out int x, out byte color, out byte background, Mat image = null)
        {
            color = 0;
            x = 0;
            background = 0;

            int offset = 1;
            int y = center_rect.Y + center_rect.Height / 2;
            bool isFind = false;
            List<byte> redPixels = new List<byte>();
            List<byte> backgroundPixels = new List<byte>();
            List<int> list_x = new List<int>();
            int endNum = 0;
            bool isEdged = false;
            while (true)
            {
                if (center_rect.Right + offset == hsv.Cols)
                {
                    isEdged = true; //搜索已经到右侧边界。这次搜索
                    break;
                }

                Vec3b pix = hsv.At<Vec3b>(y, center_rect.Right + offset);
                if (((pix[0] > 175 && pix[0] < 255) || pix[0] < 5) && pix[1] > 20)
                {
                    redPixels.Add(pix[2]);
                    list_x.Add(center_rect.Right + offset);
                    isFind = true;
                    endNum = 0;
                }
                else
                {
                    backgroundPixels.Add(pix[2]);
                    if (isFind)
                        if (++endNum > 3)
                            break;
                }
                offset++;
            }

            if (backgroundPixels.Count == 0)
                return false;

            //寻找第一遍也没有找到，重新再找一次
            if (isEdged)
            {
                background = backgroundPixels.Max();
                isEdged = false;
                offset = 3;
                list_x.Clear();
                redPixels.Clear();
                endNum = 0;
                while (true)
                {
                    if (center_rect.Right + offset == hsv.Cols)
                        break;

                    Vec3b pix = hsv.At<Vec3b>(y, center_rect.Right + offset);
                    if (pix[2] > background - 5)
                        list_x.Add(center_rect.Right + offset);
                    offset++;
                }
                int left_offset = list_x.First();
                int right_offset = list_x.Last();
                list_x.Clear();
                for (int i = left_offset; i <= right_offset; i++)
                {
                    redPixels.Add(hsv.At<Vec3b>(y, i)[2]);
                    list_x.Add(i);
                }
                if (redPixels.Count == 0)
                    return false;

                color = redPixels.Min();
                x = list_x[redPixels.IndexOf(color)];
#if true
                if (image != null)
                    Cv2.Circle(image, new Point(x, y), 5, Scalar.All(200), 3);
#endif
                return true;
            }
            else
            {
                color = redPixels.Min();
                x = list_x[redPixels.IndexOf(color)];
                background = backgroundPixels.Max();
#if true
                if (image != null)
                    Cv2.Circle(image, new Point(x, y), 5, Scalar.All(200), 3);
#endif
                return true;
            }
        }

        private static Rect CenterRegion(Mat hsv, Point center)
        {
            Vec3b template = hsv.At<Vec3b>(center.Y, center.X);
            Mat[] hsv_mv = hsv.Split();

            Mat mask_h = new Mat();
            Cv2.InRange(hsv_mv[0], template[0] - 20, template[0] + 15, mask_h);
            Mat mask_s = new Mat();
            Cv2.InRange(hsv_mv[1], template[1] - 20, 255, mask_s);
            Mat mask_v = new Mat();
            Cv2.InRange(hsv_mv[2], template[2] - 10, 255, mask_v);

            Mat mask = mask_h & mask_s & mask_v;
#if false
            if (i != -1)
                Cv2.ImWrite(string.Format("./mask/{0}.bmp", i), mask);
#endif

            Cv2.FindContours(mask, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
            List<Point> contours_total = new List<Point>();
            foreach (var item in contours)
                contours_total.AddRange(item);

            return Cv2.BoundingRect(contours_total);
        }

        private static Point LocationCenter(Mat hsv)
        {
            Vec3b pix_template = new Vec3b(108, 107, 153);
            int minDistance = HSVDistance(hsv.At<Vec3b>(0, 0), pix_template);
            Point center = new Point(0, 0);
            int distance;
            for (int y = 0; y < hsv.Rows; y++)
                for (int x = 0; x < hsv.Cols; x++)
                {
                    distance = HSVDistance(hsv.At<Vec3b>(y, x), pix_template);
                    if (distance < minDistance)
                    {
                        minDistance = distance;
                        center = new Point(x, y);
                    }
                }
            return center;
        }

        private static int HSVDistance(Vec3b pix1, Vec3b pix2)
        {
            int h = Math.Abs(pix1[0] - pix2[0]);
            int s = Math.Abs(pix1[1] - pix2[1]);
            int v = Math.Abs(pix1[2] - pix2[2]);
            if (h > 40 || v > 60)
                return int.MaxValue;
            return h * 1000 + s * 2000 + v;
        }

        private static void WhiteBalance(ref Mat image)
        {
#if true
            Mat image_resize = image.Resize(new Size(), 0.25, 0.25);
            Mat gray = image_resize.CvtColor(ColorConversionCodes.BGR2GRAY);
            Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5));
            Cv2.Erode(gray, gray, element);
            Cv2.MinMaxLoc(gray, out _, out Point maxLoc);

            Vec3b bgr = image_resize.At<Vec3b>(maxLoc.Y, maxLoc.X);

            double kb = 255 / (double)bgr[0];
            double kg = 255 / (double)bgr[1];
            double kr = 255 / (double)bgr[2];

            Mat[] mv = image.Split();
            mv[0] *= kb;
            mv[1] *= kg;
            mv[2] *= kr;
            Cv2.Merge(mv, image);
#else
            Mat[] bgr_resize = image.Resize(new Size(), 0.25, 0.25).Split();

            double b = Cv2.Mean(bgr_resize[0])[0];
            double g = Cv2.Mean(bgr_resize[1])[0];
            double r = Cv2.Mean(bgr_resize[2])[0];

            double kb = (b + g + r) / (3 * b);
            double kg = (b + g + r) / (3 * g);
            double kr = (b + g + r) / (3 * r);

            double threshold = 0.25;
            if (Math.Abs(kb - kg) > threshold || Math.Abs(kb - kr) > threshold || Math.Abs(kg - kr) > threshold)
            {
                Mat[] bgr = image.Split();
                bgr[0] *= kb;
                bgr[1] *= kg;
                bgr[2] *= kr;
                Cv2.Merge(bgr, image);
            }

            if (b < 125 && g < 125 && r < 125)
                image *= 1.2;
#endif
        }

        private static double[] m_levelTable = new double[] { 0.17, 0.33, 0.5, 0.67, 0.83, 1, 1.17, 1.33 };
    }
}
